﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using RatingAuthor;

namespace ConditionTreeTestStand.Nodes
{
  public class ConditionNodeViewModel : NodeViewModel
  {
    private readonly OperatorsLru _operatorsLru = new OperatorsLru();

    #region ' Public Properties '

    #region ' FirstOperand Property (change notification) '

    public Column FirstOperand
    {
      get { return _firstOperand; }
      set
      {
        if (value == null) throw new ArgumentNullException("value");
        if (_firstOperand == value) return;
        _firstOperand = value;
        OnFirstOperandChanged();
        NotifyOfPropertyChange(() => FirstOperand);
      }
    }

    private Column _firstOperand;

    #endregion

    #region ' OtherOperands Property (change notification) '

    public OtherOperandsVievModel OtherOperands
    {
      get { return _otherOperands; }
      set
      {
        if (_otherOperands == value) return;
        _otherOperands = value;
        NotifyOfPropertyChange(() => OtherOperands);
      }
    }

    private OtherOperandsVievModel _otherOperands;

    #endregion

    public IEnumerable<ColumnViewModel> FirstOperandPossibleValues { get; private set; }
    public override bool CanAddGroupChild { get { return false; } }
    public override bool CanAddConditionChild { get { return false; } }
    public override bool CanSwitchNodeType
    {
      get { return Parent is GroupNodeViewModel; }
    }

    #endregion

    /// <summary>Design time constructor</summary>
    public ConditionNodeViewModel()
      : base(null, null)
    {
      FirstOperand = Column.BookColumns[0];
      FirstOperandPossibleValues = Column.BookColumns.Select(c => new ColumnViewModel(this, c));
    }

    public ConditionNodeViewModel(NodeViewModel parent, NodeContext nodeContext)
      : base(parent, nodeContext)
    {
      FirstOperand = Column.BookColumns[0];
      FirstOperandPossibleValues = Column.BookColumns.Select(c => new ColumnViewModel(this, c));
    }

    #region ' Change Notification '

    protected virtual void OnFirstOperandChanged()
    {
      Operator = _operatorsLru.GetSet(Operator, FirstOperand.Type);
      OperatorPossibleValues = Operator.
        GetOperators(FirstOperand.Type).
        Select(o => new OperatorViewModel(this, o));
      switch (Operator.Arity)
      {
        case OperatorArity.Two:
          var newOne = GetOtherOperands(FirstOperand.Type);
          if (OtherOperands != null)
          {
            _valuesLru.GetSet(
              (ISingleSecondOperand)OtherOperands, 
              (ISingleSecondOperand)newOne);
          }
          OtherOperands = newOne;
          break;
        default:
          throw new NotSupportedException();
      }
    }

    private SecondOperandViewModel GetOtherOperands(OperandType operandType)
    {
      switch (operandType)
      {
        case OperandType.Bool:
          return new SecondOperandViewModel<bool>(this);
        case OperandType.Enum:
          return new SecondOperandEnumViewModel(this, NodeContext);
        case OperandType.Float:
          return new SecondOperandViewModel<double>(this);
        case OperandType.Int:
          return new SecondOperandViewModel<int>(this);
        case OperandType.String:
          return new SecondOperandStringViewModel(this);
        default: throw new Exception();
      }
    }

    protected override void OnParentChanged()
    {
      base.OnParentChanged();
      NotifyOfPropertyChange(() => CanSwitchNodeType);
    }

    #endregion

    #region ' Implementation '

    protected override NodeViewModel CreateReplacementNode()
    {
      return new GroupNodeViewModel(null, NodeContext);
    }
    public override bool TestBook(Book b)
    {
      if (!OtherOperands.IsEntered) return true;
      return (bool)Operator.Apply(new[] { FirstOperand.GetValue(b) }.Concat(OtherOperands.GetValues()));
    }
    public override Expression GetExpression()
    {
      var singleSecondOperand = OtherOperands as ISingleSecondOperand;
      if (singleSecondOperand != null)
        return Operator.GetExpression(
          new[]
            {
              FirstOperand.Expression, 
              Expression.Constant(singleSecondOperand.Value)
            });
      throw new NotImplementedException();
    }
    public override string ToString()
    {
      return "(" + FirstOperand + " " + Operator + " " + OtherOperands + ")";
    }

    #endregion

    #region ' Nested Class: LRU Operators '

    private class OperatorsLru
    {
      private readonly Dictionary<OperandType, Operator>
        _byType = new Dictionary<OperandType, Operator>();
      private readonly List<string> _flat = new List<string>();

      private void Set(Operator op)
      {
        if (_flat.Contains(op.Name))
        {
          _flat.Remove(op.Name);
        }
        _flat.Insert(0, op.Name);
        if (_flat.Count > 30)
        {
          _flat.RemoveAt(30);
        }
        _byType[op.OperandType] = op;
      }
      private Operator Get(OperandType type)
      {
        Operator res;
        if (_byType.TryGetValue(type, out res))
          return res;
        var operators = Operator.GetOperators(type);
        foreach (var name in _flat)
        {
          string hint = name;
          res = operators.FirstOrDefault(op => op.Name == hint);
          if (res != null)
            return res;
        }
        return operators.FirstOrDefault();
      }
      public Operator GetSet(Operator old, OperandType newType)
      {
        Set(old);
        return Get(newType);
      }
    }

    #endregion

    private readonly ValuesLru _valuesLru = new ValuesLru();

    private class ValuesLru
    {
      private readonly Dictionary<Type, object> _data = new Dictionary<Type, object>();
      private void Set(ISingleSecondOperand value)
      {
        var type = value.GetValueType();
        var val = value.Value;
        if (type == typeof(double))
        {
          var d = (double)val;
          if (Math.Abs(Math.Round(d) - d) < 0.000001)
          {
            val = (int) d;
            type = typeof (double);
          }
        }
        _data[type] = val;
      }
      private object Get(Type type)
      {
        object res;
        if (_data.TryGetValue(type, out res))
          return res;
        return null;
      }
      public void GetSet(ISingleSecondOperand old, ISingleSecondOperand newOne)
      {
        Set(old);
        var val = Get(newOne.GetValueType());
        if (val != null) newOne.Value = val;
      }
    }
  }
}